#region License

// This file is part of Grawlix.
// 
// Grawlix is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
// Grawlix is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public
// License along with Grawlix.  If not, see
// <http://www.gnu.org/licenses/>.

#endregion

using System;
using System.Diagnostics;
using System.Threading;
using C5;

namespace Grawlix.Services.Resources
{
	/// <summary>
	/// Stores a type of resources and acts as a hub for resource
	/// communication.
	/// </summary>
	/// <typeparam name="TDeclare">The type of resource that this
	/// class claims to store.
	/// </typeparam>
	/// <typeparam name="TDefine">The type of resource that this
	/// class actually stores.
	/// </typeparam>
	[Serializable]
	internal class ResourceRepository<TDeclare, TDefine> :
		PermanentMarshalByRefObject
		where TDeclare : IDynamicResource
		where TDefine : AbstractResourceProxy<TDeclare, TDefine>,
			TDeclare
	{
		/// <summary>
		/// The set of caches for the local <see cref="AppDomain"/>.
		/// </summary>
		private static readonly
			HashDictionary
				<ResourceRepository<TDeclare, TDefine>,
					ResourceCache<TDeclare, TDefine>> _localCaches =
						new HashDictionary
							<ResourceRepository<TDeclare, TDefine>,
								ResourceCache<TDeclare, TDefine>>();

		/// <summary>
		/// The set of active <see cref="AppDomain"/> objects.
		/// </summary>
		private readonly
			HashDictionary
				<AppDomain, ResourceCache<TDeclare, TDefine>>
			_domainCaches =
				new HashDictionary
					<AppDomain, ResourceCache<TDeclare, TDefine>>();

		/// <summary>
		/// The factory used to produce resources.
		/// </summary>
		private readonly
			AbstractResourceFactory<TDeclare, TDefine> _factory;

		/// <summary>
		/// The cache of all state information.
		/// </summary>
		private readonly HashDictionary<Uri, ResourceEntry>
			_stateCache = new HashDictionary<Uri, ResourceEntry>();

		/// <summary>
		/// Whether or not to allow set operations.
		/// </summary>
		private int _allowSet;

		/// <summary>
		/// Initializes a new repository that wraps an existing
		/// factory.
		/// </summary>
		/// <param name="factory">The factory to wrap.</param>
		/// <exception cref="ArgumentNullException"><c>factory</c> is
		/// null.</exception>
		internal ResourceRepository(
			AbstractResourceFactory<TDeclare, TDefine> factory)
		{
			if (factory == null)
				throw new ArgumentNullException("factory");
			_factory = factory;
		}

		/// <summary>
		/// Gets the creator of factory objects.
		/// </summary>
		/// <value>The factory's resource creator.</value>
		public AbstractResourceProxyCreator<TDeclare, TDefine>
			Creator
		{
			get { return _factory.Creator; }
		}

		/// <summary>
		/// Adds a cache to this repository.
		/// </summary>
		/// <param name="domain">The domain the cache was created in.</param>
		/// <param name="cache">The cache to add.</param>
		private void AddCache(AppDomain domain,
		                      ResourceCache<TDeclare, TDefine>
		                      	cache)
		{
			lock (_stateCache)
			{
				_domainCaches.Add(domain, cache);
				domain.DoCallBack(AppDomainUnloadCallBackDelegate);
			}
		}

		/// <summary>
		/// Dispatches the domain unload event from another 
		/// <see cref="AppDomain"/>.
		/// </summary>
		private void AppDomainUnloadCallBackDelegate()
		{
			AppDomain.CurrentDomain.DomainUnload +=
				(obj, args) => DomainUnload(AppDomain.CurrentDomain);
		}

		/// <summary>
		/// Unload an entire <see cref="AppDomain"/>.
		/// </summary>
		/// <param name="domain">The domain to unload.</param>
		/// <exception cref="ArgumentNullException"><c>domain</c> is
		/// null.</exception>
		private void DomainUnload(AppDomain domain)
		{
			if (domain == null)
				throw new ArgumentNullException("domain");
			lock (_stateCache)
			{
				_domainCaches.Remove(domain);
				foreach (var pair in _stateCache.ToArray())
					UnloadEntry(pair.Key, pair.Value, domain);
			}
		}

		/// <summary>
		/// Gets the cache for the specified domain.
		/// </summary>
		/// <param name="repository">The repository the cache belongs
		/// to.</param>
		/// <returns>The cache if it exists, else null.</returns>
		public static ResourceCache<TDeclare, TDefine> GetCache(
			ResourceRepository<TDeclare, TDefine> repository)
		{
			lock (_localCaches)
			{
				ResourceCache<TDeclare, TDefine> result;
				if (_localCaches.Find(repository, out result))
					return result;
				result = new ResourceCache<TDeclare, TDefine>(repository);
				repository.AddCache(AppDomain.CurrentDomain, result);
				return result;
			}
		}

		/// <summary>
		/// Reloads the resource with the specified identifier.
		/// </summary>
		/// <param name="identifier">The resource identifier.</param>
		/// <exception cref="ArgumentNullException"><c>identifier</c> is
		/// null.</exception>
		public void Reload(Uri identifier)
		{
			if (identifier == null)
				throw new ArgumentNullException("identifier");
			lock (_stateCache)
			{
				Debug.Assert(_stateCache.Contains(identifier));
				ResourceEntry entry = _stateCache[identifier];
				if (entry.IsLoading)
					_factory.CancelLoad(identifier);
				else
					entry.IsLoading = true;
				_factory.StartLoad(identifier);
			}
		}

		/// <summary>
		/// Requests the removal of a domain from the keep-alive list.
		/// </summary>
		/// <param name="domain">The domain to remove.</param>
		/// <param name="identifier">The identifier of the resource.
		/// </param>
		/// <exception cref="ArgumentNullException"><c>identifier</c>
		/// is null.</exception>
		/// <exception cref="ArgumentNullException"><c>destination</c>
		/// is null.</exception>
		public void Remove(AppDomain domain, Uri identifier)
		{
			if (identifier == null)
				throw new ArgumentNullException("identifier");
			if (domain == null)
				throw new ArgumentNullException("domain");
			lock (_stateCache)
			{
				if (_stateCache.Contains(identifier))
				{
					ResourceEntry entry = _stateCache[identifier];
					if (_domainCaches[domain].ResourcePresent(identifier))
						UnloadEntry(identifier, entry, domain);
				}
			}
		}

		/// <summary>
		/// Sets the state.
		/// </summary>
		/// <param name="identifier">The identifier.</param>
		/// <param name="state">The state.</param>
		/// <param name="loadFailed">if set to <c>true</c> [load
		/// failed].</param>
		/// <exception cref="ArgumentNullException"><c>identifier</c>is
		/// null.</exception>
		/// <exception cref="InvalidOperationException">Attempt to
		/// update outside of an <see cref="Update(TimeSpan)"/> call.
		/// </exception>
		/// <exception cref="ArgumentException">Attempt to update a
		/// nonexistent identifier.</exception>
		/// <exception cref="ArgumentException">Both state is not null
		/// and <paramref name="loadFailed"/> is true.</exception>
		public void SetState(Uri identifier, object state,
		                     bool loadFailed)
		{
			if (identifier == null)
				throw new ArgumentNullException("identifier");
			if (Thread.VolatileRead(ref _allowSet) != 1)
			{
				throw new InvalidOperationException(
					"Can only update a factory's state during " +
					"the duration of the Update call. " +
					"Ensure that SetState is only called when " +
					"nested in Update.");
			}
			if (!_stateCache.Contains(identifier))
			{
				throw new ArgumentException(
					"The resource repository for " + typeof(TDeclare).Name +
					" has no entry for " + identifier + ". " +
					"Ensure that only valid resources are updated.",
					"identifier");
			}
			if ((state != null) && loadFailed)
			{
				throw new ArgumentException(
					"Received a report that the load failed, but " +
					"still given a state object. " +
					"Ensure that either [state] is null or " +
					"[loadFailed] is false.", "state");
			}
			ResourceEntry entry = _stateCache[identifier];
			if (entry.State != null)
				_factory.StartUnload(entry.State);
			entry.State = state;
			entry.LoadFailed = loadFailed;
			entry.IsLoading = false;
			var state1 = new ResourceState
				{StateData = state, LoadFailed = loadFailed};
			ResourceState resourceState = state1;
			foreach (var domain in entry.AppDomainList)
				_domainCaches[domain].UpdateState(identifier,
				                                  resourceState);
		}

		/// <summary>
		/// Implementation of unloading.
		/// </summary>
		/// <param name="identifier">The identifier.</param>
		/// <param name="entry">The entry.</param>
		/// <param name="domain">The domain.</param>
		private void UnloadEntry(Uri identifier,
		                         ResourceEntry entry,
		                         AppDomain domain)
		{
			entry.RemoveEnvironment(domain);
			if (entry.IsEmpty)
			{
				_stateCache.Remove(identifier);
				if (entry.IsLoading)
					_factory.CancelLoad(identifier);
				if (entry.State != null)
					_factory.StartUnload(entry.State);
			}
		}

		/// <summary>
		/// Requests that a resource be updated on a particular 
		/// <see cref="AppDomain"/>.
		/// </summary>
		/// <param name="appDomain">The <see cref="AppDomain"/> which
		/// needs an update.</param>
		/// <param name="destination">The destination cache.</param>
		/// <param name="identifier">The identifier of the resource to
		/// update.</param>
		/// <exception cref="ArgumentNullException"><c>identifier</c>
		/// is null.</exception>
		/// <exception cref="ArgumentNullException"><c>destination</c>
		/// is null.</exception>
		/// <exception cref="ArgumentNullException"><c>appDomain</c> is
		/// null.</exception>
		public void Update(AppDomain appDomain,
		                   ResourceCache<TDeclare, TDefine>
		                   	destination, Uri identifier)
		{
			if (identifier == null)
				throw new ArgumentNullException("identifier");
			if (destination == null)
				throw new ArgumentNullException("destination");
			if (appDomain == null)
				throw new ArgumentNullException("appDomain");
			lock (_stateCache)
			{
				ResourceEntry entry;
				if (_stateCache.Contains(identifier))
					entry = _stateCache[identifier];
				else
				{
					entry = new ResourceEntry();
					_stateCache[identifier] = entry;
					entry.IsLoading = true;
					_factory.StartLoad(identifier);
				}
				entry.AddEnvironment(appDomain);
				var state = new ResourceState
					{
						StateData = entry.State, LoadFailed = entry.LoadFailed
					};
				destination.UpdateState(identifier, state);
			}
		}

		/// <summary>
		/// Updates the specified elapsed.
		/// </summary>
		/// <param name="elapsed">The elapsed.</param>
		public void Update(TimeSpan elapsed)
		{
			lock (_stateCache)
			{
				Thread.VolatileWrite(ref _allowSet, 1);
				_factory.UpdateState(elapsed);
				Thread.VolatileWrite(ref _allowSet, 0);
			}
		}
	}
}